﻿using System.Collections.Generic;
using System.Linq;
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Security;
using System.Security.Permissions;
using System.Security.Policy;

namespace MyUtils
{
	public static class DynamicClassFactory
	{
		static ModuleBuilder _moduleBuilder;

		static ModuleBuilder ModuleBuilder
		{
			get
			{
				if (_moduleBuilder == null)
				{
					var myDomain = AppDomain.CurrentDomain;
					var asmName = new AssemblyName {Name = "DynamicAssembly"};
					var myAsmBuilder = myDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndCollect/*, myDomain.Evidence*/);
					var myModule = myAsmBuilder.DefineDynamicModule(asmName.Name);
					_moduleBuilder = myModule;
				}
				return _moduleBuilder;
			}
		}

		public static TypeBuilder CreateClass(string name)
		{
			var myModule = ModuleBuilder;
			// let's create unique classes, becasue someone can create delegate with the same name
			return myModule.DefineType(name + "_" + Guid.NewGuid().ToString("N"));
		}

		static readonly MethodInfo _methodWeakReferenceGetTarget;

		static DynamicClassFactory()
		{
			_methodWeakReferenceGetTarget = typeof(WeakReference).GetMethod("get_Target");
		}

		static readonly Dictionary<Type, Type> _cacheWeakDelegate = new Dictionary<Type, Type>(64);

		static Type CachedWeakDelegateClass(MethodInfo methodInfo, Type delegateType, Type unsubscribeDelegateType)
		{
			Type result;
			if (!_cacheWeakDelegate.TryGetValue(delegateType, out result))
			{
				_cacheWeakDelegate[delegateType] = result = CreateWeakDelegateClass(methodInfo, delegateType, unsubscribeDelegateType);
			}
			return result;
		}

		private static Type CreateWeakDelegateClass(MethodInfo methodInfo, Type delegateType, Type unsubscribeDelegateType)
		{
			using (P.S("CreateWeakDelegateClass"))
			{
				var parameters = methodInfo.GetParameters().Select(x => x.ParameterType).ToArray();

				#region Class

				var clazz = CreateClass(delegateType.Name);
				var weakTarget = clazz.DefineField("_weakTarget", typeof(WeakReference), FieldAttributes.Private);
				var ptr = clazz.DefineField("_ptr", typeof(IntPtr), FieldAttributes.Private);
				var unsubscribe = clazz.DefineField("_unsubscribe", unsubscribeDelegateType, FieldAttributes.Private);

				#region Constructor

				var ctor = clazz.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(WeakReference), typeof(IntPtr), unsubscribeDelegateType });
				var il = ctor.GetILGenerator();

				// _weakTarget = weakTarget;
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldarg_1);
				il.Emit(OpCodes.Stfld, weakTarget);

				// _ptr = ptr;
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldarg_2);
				il.Emit(OpCodes.Stfld, ptr);

				// _unsubscribe = unsubscribe;
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldarg_3);
				il.Emit(OpCodes.Stfld, unsubscribe);

				il.Emit(OpCodes.Ret);

				#endregion

				#region Method Invoke

				var methodInvoke = clazz.DefineMethod("Invoke", MethodAttributes.Public, CallingConventions.HasThis, methodInfo.ReturnType, parameters);
				il = methodInvoke.GetILGenerator();

				il.DeclareLocal(typeof(object)); // object target;
				var elseLabel = il.DefineLabel();
				var exitLabel = il.DefineLabel();

				il.Emit(OpCodes.Ldarg_0); // this...
				il.Emit(OpCodes.Ldfld, weakTarget); // this._weakTarget...
				il.Emit(OpCodes.Callvirt, _methodWeakReferenceGetTarget); // this._weakTarget.Target... (get_Target - is a virtual Method of a property accessor)
				il.Emit(OpCodes.Stloc_0); // target = this._weakTarget.Target

				il.Emit(OpCodes.Ldloc_0); // target
				il.Emit(OpCodes.Brfalse_S, elseLabel); // if (target == null) goto elseLabel;

				// create TDelegate(_weakTarget.Target, _ptr)
//				il.Emit(OpCodes.Ldloc_0); // target
//				il.Emit(OpCodes.Ldarg_0); // this.
//				il.Emit(OpCodes.Ldfld, ptr); // this._prt
//				il.Emit(OpCodes.Newobj, delegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) })); // create specified Delegate with ctor(object, native int)

				il.Emit(OpCodes.Ldloc_0); // target // call next method for alive target

				var parametersCount = parameters.Length;
				for (int i = 1; i <= parametersCount; i++)
				{
					il.Emit(OpCodes.Ldarg, i); // load n-th argument
				}

				// dirrect access, without JIT Visibility Checking
				il.Emit(OpCodes.Ldarg_0); // this
				il.Emit(OpCodes.Ldfld, ptr); // this._ptr
				il.EmitCalli(OpCodes.Calli, CallingConventions.HasThis | CallingConventions.Standard, methodInfo.ReturnType, parameters, null);
				//il.Emit(methodInfo.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, methodInfo);
				//il.Emit(OpCodes.Calli);
				//il.Emit(OpCodes.Call);

				//il.Emit(OpCodes.Callvirt, delegateType.GetMethod("Invoke"));

				// il.EmitWriteLine("Alive!");

				il.Emit(OpCodes.Ret); // return result if any. Stack is consistent (our return matches methodInfo).

				il.MarkLabel(elseLabel); // elseLabel: // label for our 'if-elseif-else'

				// else if (_unsubscribe != null)
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldfld, unsubscribe);
				il.Emit(OpCodes.Brfalse_S, exitLabel);

				// if true block
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldfld, unsubscribe);
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldftn, methodInvoke);

				il.Emit(OpCodes.Newobj, delegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) })); // create specified Delegate with ctor(object, IntPtr)
				il.Emit(OpCodes.Callvirt, unsubscribeDelegateType.GetMethod("Invoke")); // _unsubscribe(Invoke) // call unsubscribe delegate

				il.MarkLabel(exitLabel); // exitLabel: // label for our 'if-elseif-else'

				// il.EmitWriteLine("Dead!");
				if (methodInfo.ReturnType != typeof(void))
				{
					il.Emit(OpCodes.Ldnull);
					// TODO emit default value for value types
					// throw new NotImplementedException("Delegate with return value");
				}
				il.Emit(OpCodes.Ret); // target is died. Return default value. If non-void method, default value should be on stack.

				#endregion

				#region Factory Method

				var factory = clazz.DefineMethod("Create", MethodAttributes.Static | MethodAttributes.Public, delegateType, new[] { typeof(WeakReference), typeof(IntPtr), unsubscribeDelegateType });
				il = factory.GetILGenerator();
				il.DeclareLocal(clazz); // Gen gen;

				// gen = new Gen(WeakReference weakTarget, IntPtr ptr, Action<T> unsubscribe);
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldarg_1);
				il.Emit(OpCodes.Ldarg_2);
				il.Emit(OpCodes.Newobj, ctor);
				il.Emit(OpCodes.Stloc_0);

				// return gen.Invoke; // return reference to method
				il.Emit(OpCodes.Ldloc_0); // gen.
				il.Emit(OpCodes.Ldftn, methodInvoke); // gen.Invoke
				il.Emit(OpCodes.Newobj, delegateType.GetConstructor(new[] {typeof(object), typeof(IntPtr)})); // create specified Delegate with ctor(object, IntPtr)
				il.Emit(OpCodes.Ret);

				#endregion

				#endregion

				var type = clazz.CreateType();

				return type;
			}
		}

		static readonly Dictionary<MethodInfo, Type> _cacheSubscriberActionClosure = new Dictionary<MethodInfo, Type>(64);

		public static Type CachedCreateSubscriberActionClosureClass(Type delegateType, Type publisherType, MethodInfo subscribeAccessor, MethodInfo unsubscribeAccessor)
		{
			Type result;
			if (!_cacheSubscriberActionClosure.TryGetValue(subscribeAccessor, out result))
			{
				_cacheSubscriberActionClosure[subscribeAccessor] = result = CreateSubscriberActionClosureClass(delegateType, publisherType, subscribeAccessor, unsubscribeAccessor);
			}
			return result;
		}

		static Type CreateSubscriberActionClosureClass(Type delegateType, Type publisherType, MethodInfo subscribeAccessor, MethodInfo unsubscribeAccessor)
		{
			#region Class

			var clazz = CreateClass(delegateType.Name + "_Closure");
			var publisher = clazz.DefineField("_publisher", publisherType, FieldAttributes.Public);

			#region Method MagicSubscribe

			var subscribe = clazz.DefineMethod("MagicSubscribe", MethodAttributes.Public, CallingConventions.HasThis, typeof(void), new[] { delegateType });

			var il = subscribe.GetILGenerator();
			il.Emit(OpCodes.Ldarg_0); // this
			il.Emit(OpCodes.Ldfld, publisher); // this._publisher
//			var mi = publisherType.GetMethod("Test");
//			il.Emit(mi.IsVirtual ? OpCodes.Callvirt : OpCodes.Call, mi);
			il.Emit(OpCodes.Ldarg_1); // handler // argument of a method
			il.Emit(OpCodes.Callvirt, subscribeAccessor); // this._publisher.add_SomEvent(handler)
			il.Emit(OpCodes.Ret);

			#endregion

			#region Method Unsubscribe

			var unsubscribe = clazz.DefineMethod("Unsubscribe", MethodAttributes.Public, CallingConventions.HasThis, typeof(void), new[] { delegateType });
			il = unsubscribe.GetILGenerator();
			il.Emit(OpCodes.Ldarg_0); // this
			il.Emit(OpCodes.Ldfld, publisher); // this._publisher
			il.Emit(OpCodes.Ldarg_1); // handler // argument of a method
			il.Emit(OpCodes.Callvirt, unsubscribeAccessor); // this._publisher.remove_SomEvent(handler)
			il.Emit(OpCodes.Ret);

			#endregion

			#endregion

			return clazz.CreateType();
		}

		// generic is required for Dynamic Invoke Optimizer
		public static T GetWeakDelegate<T>(Delegate strongDelegate, Action<T> unsubscribe = null) where T : class
		{
			return GetWeakDelegate<T>(strongDelegate.Method, strongDelegate.GetType(), strongDelegate.Target, unsubscribe);
		}

		// generic is required for Dynamic Invoke Optimizer
		public static T GetWeakDelegate<T>(MethodInfo methodInfo, Type delegateType, object target, Action<T> unsubscribe = null) where T : class
		{
			Type type;
			using (P.S("WD_CachedWeakDelegateClass")) // 4 ms
			{
				type = CachedWeakDelegateClass(methodInfo, delegateType, typeof(Action<T>));
			}

			MethodInfo factoryMethodInfo;
			using (P.S("WD_GetMethod")) // 10 ms for 10000 subscriptions
			{
				factoryMethodInfo = type.GetMethod("Create", BindingFlags.Static | BindingFlags.Public);
			}

			Func<WeakReference, IntPtr, Action<T>, T> factoryMethod;
			using (P.S("WD_Compile")) // 7 ms
			{
				factoryMethod = factoryMethodInfo.Compile<Func<WeakReference, IntPtr, Action<T>, T>>();
			}

			using (P.S("WD_Factory")) // 18 ms
			{
				return factoryMethod(new WeakReference(target), methodInfo.MethodHandle.GetFunctionPointer(), unsubscribe);
			}
		}

	}
}
